Skip to content

fix: detect font-scoped garbled text - #365

Open
AnnaSuSu wants to merge 9 commits into
firecrawl:mainfrom
AnnaSuSu:agent/fix-font-scoped-garble-stats
Open

fix: detect font-scoped garbled text#365
AnnaSuSu wants to merge 9 commits into
firecrawl:mainfrom
AnnaSuSu:agent/fix-font-scoped-garble-stats

Conversation

@AnnaSuSu

@AnnaSuSu AnnaSuSu commented Aug 12, 2026

Copy link
Copy Markdown

Fixes #352.

Root cause

analyze_text_quality accumulated a single CipherGarbleStats sample for the whole page. On mixed-font pages, enough healthy text from one font could dilute shifted or garbled text from another font, so the page-wide histogram passed and corrupt text could be served silently.

Fix

  • Keep the existing page-wide accumulator as a fallback.
  • Add per-font accumulators keyed by TextItem.font; empty font names remain page-wide only.
  • Flag the page when either the page-wide sample or a sufficiently large, prose-eligible per-font sample meets the existing cipher thresholds.
  • Require broad alphabet coverage for per-font samples.
  • Treat letter-only samples as cipher-eligible. When a sample contains digits or symbols, additionally require multiple short word-like tokens, including PDFs that emit one visible word per TextItem without a trailing space.
  • Evaluate Base64 over the complete per-font sample while ignoring whitespace and TextItem boundaries. Exempt only valid Base64 framing with either Base64-specific symbols/padding or an overwhelmingly printable decoded payload.

The aggregate structured-data gate replaces the earlier broad Base64 percentage exemption. It preserves digit-heavy shifted tables and word-fragmented PDFs without allowing encoded chunks to become false prose evidence. This does not change the public API, page-wide cipher thresholds, or the runtime semantics of classify_pdf* / detect_pdf*.

API boundary

Fast classify_pdf* and detect_pdf* calls remain structural classifiers: they do not extract text or validate font/character encoding, so pages_needing_ocr=[] is not an encoding-quality verdict. Encoding-quality OCR routing uses extract_pages_markdown* or the full process_pdf* APIs; Rust callers can also select ProcessMode::Analyze to run text-quality analysis without Markdown generation.

Commit 4644ac9 makes this contract explicit in the Rust API docs, Python binding docs and type stubs, the Python guide, the Rust guide, and the README. An opt-in analyzed-classification API can be considered separately if maintainers want that additional public surface.

Regression coverage

  • Healthy Helvetica prose plus a shifted FixtureFont sample is flagged as suspected_garbled_text.
  • Digit/identifier-heavy shifted prose is flagged both as one string and as separate word-sized TextItems.
  • Clean multi-font pages and structured code/acronym fonts remain accepted.
  • Binary and prose Base64 remain accepted as complete strings, 16-character PDF item fragments, whitespace-separated 16-character segments, and MIME-style 76-character wrapped lines.
  • Letter-only uninterrupted shifted prose, uniform-case shifts, and cross-font page-wide accumulation remain covered.

The word-item garble and short-wrapped Base64 regressions both failed before d3ad416 and pass with the aggregate discriminator.

I also reran the self-contained ReportLab/pypdf reproduction from #352 against the release detect-pdf --analyze --json path after merging current main (74ebce4), which now reports actual BaseFont family names on TextItem.font:

  • Clean mixed-font PDF: no OCR pages and no encoding issue.
  • Same mixed-font PDF with the embedded Vera font's ToUnicode map shifted: page 1 is routed to OCR with suspected_garbled_text.
  • Clean table-only PDF: no OCR pages and no encoding issue.
  • Same table-only PDF with the ToUnicode map shifted: page 1 is routed to OCR with suspected_garbled_text.

Current-main sync

Merge commit be0267e resolves the documentation conflict with current main while preserving both sides of the API contract: the newly added selective process_pdf_with_ocr* APIs are documented, and fast classify_pdf* / detect_pdf* calls remain explicitly structural-only. The PR remains limited to its intended seven-file delta against current main.

Verification

  • cargo fmt --all -- --check
  • cargo fmt --manifest-path wasm/Cargo.toml -- --check
  • cargo clippy -- -D warnings
  • cargo clippy --features ocr -- -D warnings
  • cargo test --quiet (965 library, 3 binary, 162 integration, and 2 doc tests passed)
  • cargo test --features ocr (1046 library, 4 binary, 162 integration, 2 local-render, 5 OCR, and 2 doc tests passed)
  • cargo build --release
  • python3 scripts/version.py --check (all packages at 1.15.0)
  • python3 -m unittest discover -s scripts/tests (18 tests passed)
  • cargo check --manifest-path wasm/Cargo.toml
  • git diff --check
  • Issue Mixed healthy and garbled fonts bypass the page-level CipherGarbleStats OCR signal #352 clean/broken mixed-font and table-only PDF A/B reproductions with the release binary

pdf-evals was not run because firecrawl/pdf-evals is not accessible to the current GitHub account (404). The installed Homebrew Rust toolchain also lacks the wasm32-unknown-unknown target and has no rustup, so wasm32-target checks could not be rerun locally; the wasm package native check and both format checks pass. Maintainer-side pdf-evals and wasm32 CI would still be appreciated.

AI assistance: OpenAI Codex helped inspect the aggregation path, implement the focused fix, and run validation. I reviewed the complete diff and take responsibility for the contribution.

@AnnaSuSu
AnnaSuSu marked this pull request as ready for review August 12, 2026 08:25
Copilot AI lite review requested due to automatic review settings August 12, 2026 08:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes mixed-font pages bypassing garbled-text detection by augmenting analyze_text_quality with per-font CipherGarbleStats accumulation (while retaining the existing page-wide aggregate as a backstop), addressing the dilution issue described in #352.

Changes:

  • Track substitution-cipher/garble statistics both page-wide and per-font (keyed by TextItem.font), skipping empty font names in the per-font map while still counting them in the page-wide aggregate.
  • Route a page to OCR if either the page-wide stats or any statistically meaningful per-font stats meet the existing “looks garbled” thresholds.
  • Add regression tests covering mixed healthy+garbled fonts, clean multi-font pages, and cross-font aggregation behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/text_quality.rs Adds per-font CipherGarbleStats accumulation and updates OCR-routing to consider both page-wide and per-font evidence.
src/lib.rs Adds targeted unit tests validating mixed-font garble detection and preserving prior aggregation behavior across items/fonts.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files

Shadow auto-approve: would not auto-approve because issues were found.
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Fix all with cubic | Re-trigger cubic

Comment thread src/text_quality.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Shadow auto-approve: would not auto-approve because issues were found.

Fix all with cubic | Re-trigger cubic

Comment thread src/text_quality.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread src/text_quality.rs Outdated
Comment thread src/text_quality.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread src/text_quality.rs Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread src/text_quality.rs
Comment thread src/text_quality.rs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Shadow auto-approve: would auto-approve. Focused bug fix adds per-font cipher-garble detection to catch mixed-font garbled text; preserves existing thresholds and public API, and adds comprehensive regression tests pinning the corrected behavior.

Re-trigger cubic

@knealfortegra

knealfortegra commented Aug 13, 2026

Copy link
Copy Markdown

One consumer/API boundary worth making explicit: my comparison harness evaluates classify_pdf_bytes() as an OCR router separately from extract_pages_markdown_bytes(). At this PR's head, the former still calls classify_pdf_mem()detector::detect_from_document(), so neither the table-only nor mixed-font fixture is routed to OCR there, while Markdown extraction now catches the mixed-font case.

I agree the default fast classifier should not silently become a full extraction. Is the intended contract that callers needing encoding-quality routing use the extraction API, or would maintainers prefer a separate follow-up for an opt-in analyzed-classification API (text-quality routing without Markdown generation)? Either is workable; documenting that boundary would help consumers avoid treating pages_needing_ocr=[] from the fast classifier as a text-quality verdict.

@AnnaSuSu

Copy link
Copy Markdown
Author

@knealfortegra Thanks for calling this out. I confirmed the boundary and documented it in 4644ac9.

For this PR, the current contract remains: classify_pdf* and detect_pdf* are fast structural classifiers. They do not extract text or validate font/character encoding, so pages_needing_ocr=[] must not be interpreted as an encoding-quality verdict. Callers that need encoding-quality OCR routing should use extract_pages_markdown* or the full process_pdf* APIs; Rust callers can also use ProcessMode::Analyze to run extraction and text-quality analysis without generating Markdown.

I updated the Rust public API docs, Python binding docs and type stubs, both language guides, the README, and the PR description. I deliberately did not make the fast classifier perform extraction. If maintainers want a separate opt-in analyzed-classification API—particularly for Python callers that want text-quality routing without Markdown—that would be a clean follow-up rather than an expansion of this focused fix.

The documentation-only follow-up passed formatting, clippy, the full Rust test suite (883 library, 2 binary, 162 integration, 2 doc tests), release build, version check, and the native WASM package check.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 6 files (changes from recent commits).

Shadow auto-approve: would require human review. Adds per-font garble heuristics and Base64 aggregate discriminator, changing which pages are flagged for OCR routing; heuristic thresholds and policy are product tradeoffs needing human review. Diff truncation also prevents verifying the full Base64 logic.

Re-trigger cubic

@AnnaSuSu

Copy link
Copy Markdown
Author

Resolved the main conflict in be0267e and reran the current test matrix locally, including default/OCR clippy, default/OCR tests, release build, script tests, native WASM check, and the clean/broken mixed-font + table-only #352 reproductions. All passed. Cubic is green and there are no unresolved review threads.

The repository CI run is currently waiting for maintainer approval because this is an external-contributor PR: https://github.com/firecrawl/pdf-inspector/actions/runs/32109052130

Could a maintainer approve that run when convenient?

@AnnaSuSu

Copy link
Copy Markdown
Author

Synced this branch with current main in 1cc9204 and resolved the src/lib.rs conflict by keeping both the current supplemental-OCR tests and this PR's font-scoped test helper. The refreshed branch passes root and wasm formatting, default and OCR-feature Clippy with -D warnings, default tests (1041 library, 3 binary, 165 integration, 2 doc), OCR-feature tests (1129 library, 4 binary, 165 integration, 2 local-render, 5 OCR, 2 doc), release build, version-sync check, 18 script tests, and native wasm cargo check. @abimaelmartell, could you please review the updated branch and approve the external-contributor CI run when convenient? The PR is mergeable again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mixed healthy and garbled fonts bypass the page-level CipherGarbleStats OCR signal

3 participants